/*
 * Copyright (C) 2016 Bilibili
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.bilibili.draweetext;

import static com.bilibili.draweetext.util.Constants.INT_N_1;

import com.bilibili.draweetext.util.Constants;
import com.bilibili.draweetext.util.TextUtils;
import com.facebook.base.DrawableWithCaches;
import com.facebook.common.executors.UiThreadImmediateExecutorService;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.logging.FLog;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.BaseDataSubscriber;
import com.facebook.datasource.DataSource;
import com.facebook.datasource.DataSubscriber;
import com.facebook.drawee.components.DeferredReleaser;
import com.facebook.drawee.drawable.ForwardingDrawable;
import com.facebook.drawee.drawable.OrientedDrawable;
import com.facebook.imagepipeline.common.ImageDecodeOptions;
import com.facebook.imagepipeline.core.ImagePipelineFactory;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.image.CloseableStaticBitmap;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;

import ohos.agp.colors.RgbColor;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.utils.Rect;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.image.PixelMap;
import ohos.utils.net.Uri;

/**
 * Like {@link com.facebook.drawee.interfaces.DraweeHierarchy} that displays a placeholder
 * until actual image is set.
 * <p/>
 * Usage in DraweeTextView's text.
 *
 * @author yrom
 */
public class DraweeSpan implements DeferredReleaser.Releasable {
    private static final int HILOG_TYPE = 3;

    private static final int HILOG_DOMAIN = 0xD000F00;

    private static final HiLogLabel LABEL = new HiLogLabel(HILOG_TYPE, HILOG_DOMAIN, "[DraweeSpan] ");

    private Point mLayout = new Point();

    private Rect mMargin = new Rect();

    private Element mDrawable;

    private Element mPlaceHolder;

    private DraweeTextView mAttachedView;

    private String mImageUri;

    private final DeferredReleaser mDeferredReleaser;

    private final ForwardingDrawable mActualDrawable;

    private CloseableReference<CloseableImage> mFetchedImage;

    private DataSource<CloseableReference<CloseableImage>> mDataSource;

    private boolean isAttached;

    private boolean isRequestSubmitted;

    protected DraweeSpan(String uri, Element placeHolder) {
        mImageUri = uri;
        mDeferredReleaser = DeferredReleaser.getInstance();
        mPlaceHolder = placeHolder;
        // create forwarding drawable with placeholder
        mActualDrawable = new ForwardingDrawable(mPlaceHolder);
    }

    /**
     * set bounds to the Element
     */
    protected void layout() {
        mActualDrawable.getDrawable().setBounds(0, 0, mLayout.getPointXToInt(), mLayout.getPointYToInt());
    }

    @Override
    public void release() {
        isRequestSubmitted = false;
        isAttached = false;
        mAttachedView = null;
        if (mDataSource != null) {
            mDataSource.close();
            mDataSource = null;
        }
        if (mDrawable != null) {
            releaseDrawable(mDrawable);
        }
        mDrawable = null;
        if (mFetchedImage != null) {
            CloseableReference.closeSafely(mFetchedImage);
            mFetchedImage = null;
        }
    }

    /**
     * Submits request to load the image
     *
     * @param view DraweeText
     */
    public void onAttach(DraweeTextView view) {
        isAttached = true;
        if (mAttachedView != view) {
            mActualDrawable.setCallback(null);
            if (mAttachedView != null) {
                HiLog.error(LABEL, "has been attached to view:" + mAttachedView);
                return;
            }
            mAttachedView = view;
            setDrawableInner(mDrawable);
        }
        mDeferredReleaser.cancelDeferredRelease(this);
        if (!isRequestSubmitted) {
            submitRequest();
        }
    }

    /**
     * Obtains the hashCode of the image as an id
     *
     * @return id for the imageUri
     */
    protected String getId() {
        return String.valueOf(getImageUri().hashCode());
    }

    private void submitRequest() {
        if (TextUtils.isEmpty(getImageUri())) {
            return;
        }

        isRequestSubmitted = true;
        final String id = getId();
        mDataSource = fetchDecodedImage();
        DataSubscriber<CloseableReference<CloseableImage>> subscriber;
        subscriber = new BaseDataSubscriber<CloseableReference<CloseableImage>>() {
            @Override
            protected void onNewResultImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
                boolean isFinished = dataSource.isFinished();
                CloseableReference<CloseableImage> result = dataSource.getResult();
                if (result != null) {
                    onNewResultInternal(id, dataSource, result, isFinished);
                } else if (isFinished) {
                    onFailureInternal(id, dataSource, new NullPointerException(), /* isFinished */ true);
                }
            }

            @Override
            protected void onFailureImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
                onFailureInternal(id, dataSource, dataSource.getFailureCause(), /* isFinished */ true);
            }

            @Override
            public void onNewResult(DataSource<CloseableReference<CloseableImage>> dataSource) {
                super.onNewResult(dataSource);
            }

            @Override
            public void onCancellation(DataSource<CloseableReference<CloseableImage>> dataSource) {
                super.onCancellation(dataSource);
            }

            @Override
            public void onProgressUpdate(DataSource<CloseableReference<CloseableImage>> dataSource) {
                super.onProgressUpdate(dataSource);
            }

            @Override
            public void onFailure(DataSource<CloseableReference<CloseableImage>> dataSource) {
                super.onFailure(dataSource);
            }
        };
        mDataSource.subscribe(subscriber, UiThreadImmediateExecutorService.getInstance());
    }

    /**
     * Obtains the decoded image from uri
     *
     * @return decoded image from image uri
     */
    @VisibleForTesting
    protected DataSource<CloseableReference<CloseableImage>> fetchDecodedImage() {
        ImagePipelineFactory factory;
        try {
            factory = ImagePipelineFactory.getInstance();
        } catch (NullPointerException e) {
            // Image pipeline is not initialized
            ImagePipelineFactory.initialize(mAttachedView.getContext().getApplicationContext());
            factory = ImagePipelineFactory.getInstance();
        }
        ImageRequest request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(getImageUri()))
            .setImageDecodeOptions(ImageDecodeOptions.newBuilder().setDecodePreviewFrame(true).build())
            .build();
        return factory.getImagePipeline().fetchDecodedImage(request, null);
    }

    /**
     * Obtains the image uri
     *
     * @return uri of the image
     */
    public String getImageUri() {
        return mImageUri;
    }

    /**
     * Obtains the Element from ForwardingDrawable object
     *
     * @return Element object
     */
    public Element getDrawable() {
        return mActualDrawable.getDrawable();
    }

    private void setDrawableInner(Element drawable) {
        if (drawable == null) {
            return;
        }
        if (mAttachedView == null) {
            mActualDrawable.setCallback(null);
            release();
            return;
        }
        mActualDrawable.setDrawable(drawable);
        mActualDrawable.getDrawable().setBounds(0, 0, mLayout.getPointXToInt(), mLayout.getPointYToInt());
        mAttachedView.notifyElementChanged();
    }

    private void onNewResultInternal(String id, DataSource<CloseableReference<CloseableImage>> dataSource,
        CloseableReference<CloseableImage> result, boolean isFinished) {
        // ignored this result
        if (!getId().equals(id) || dataSource != mDataSource || !isRequestSubmitted) {
            CloseableReference.closeSafely(result);
            dataSource.close();
            return;
        }

        Element drawable;
        drawable = createDrawable(result);

        CloseableReference previousImage = mFetchedImage;
        Element previousDrawable = mDrawable;
        mFetchedImage = result;
        try {
            // set the new image
            if (isFinished) {
                mDataSource = null;
                setImage(drawable);
            }
        } finally {
            if (previousDrawable != null && previousDrawable != drawable) {
                releaseDrawable(previousDrawable);
            }
            if (previousImage != null && previousImage != result) {
                CloseableReference.closeSafely(previousImage);
            }
        }
    }

    private void onFailureInternal(String id, DataSource<CloseableReference<CloseableImage>> dataSource,
        Throwable throwable, boolean isFinished) {
        if (FLog.isLoggable(HiLog.WARN)) {
            FLog.w(DraweeSpan.class, id + " load failure", throwable);
        }
        // ignored this result
        if (!getId().equals(id) || dataSource != mDataSource || !isRequestSubmitted) {
            dataSource.close();
            return;
        }
        if (isFinished) {
            mDataSource = null;
            // Set the previously available image if available.
            setDrawableInner(mDrawable);
        }
    }

    private Element createDrawable(CloseableReference<CloseableImage> result) {
        CloseableImage closeableImage = result.get();
        if (closeableImage instanceof CloseableStaticBitmap) {
            CloseableStaticBitmap closeableStaticBitmap = (CloseableStaticBitmap) closeableImage;
            PixelMapElement pixelMapElement = createPixelMapElement(closeableStaticBitmap.getUnderlyingBitmap());

            return closeableStaticBitmap.getRotationAngle() != 0 && closeableStaticBitmap.getRotationAngle() != INT_N_1
                ? new OrientedDrawable(pixelMapElement, closeableStaticBitmap.getRotationAngle())
                : pixelMapElement;
        }
        throw new UnsupportedOperationException("Unrecognized image class: " + closeableImage);
    }

    /**
     * Obtains the PixelMapElement from PixelMap object
     *
     * @param pixelMap pixelMap input to generate PixelMapElement
     * @return PixelMapElement generated using pixelMap
     */
    protected PixelMapElement createPixelMapElement(PixelMap pixelMap) {
        PixelMapElement drawable = null;
        if (mAttachedView != null) {
            drawable = new PixelMapElement(pixelMap);
        }
        return drawable;
    }

    /**
     * Sets element object obtained using image uri
     *
     * @param element new element object obtained using image uri
     */
    public void setImage(Element element) {
        if (mDrawable != element) {
            releaseDrawable(mDrawable);
            setDrawableInner(element);
            mDrawable = element;
        }
    }

    void releaseDrawable(Element drawable) {
        if (drawable instanceof DrawableWithCaches) {
            ((DrawableWithCaches) drawable).dropCaches();
        }
    }

    /**
     * Removes listener
     */
    public void onDetach() {
        if (!isAttached) {
            return;
        }
        mActualDrawable.setCallback(null);
        mAttachedView = null;
        reset();
        mDeferredReleaser.scheduleDeferredRelease(this);
    }

    /**
     * Set placeHolder to Element
     */
    public void reset() {
        setDrawableInner(mPlaceHolder);
    }

    /**
     * SimpleDraweeSpan builder.
     */
    public static class Builder {
        String uri;

        int width = Constants.WIDTH;

        int height = Constants.HEIGHT;

        Element mPlaceholder;

        Rect margin = new Rect();

        public Builder(String uri) {
            this.uri = uri;
            if (uri == null) {
                throw new NullPointerException("Attempt to create DraweeSpan with null uri string!");
            }
        }

        /**
         * sets the dimensions for the element
         *
         * @param widthInt width of this span, px
         * @param heightInt height of this span, px
         * @return class instance
         */
        public DraweeSpan.Builder setLayout(int widthInt, int heightInt) {
            width = widthInt;
            height = heightInt;
            return this;
        }

        /**
         * Sets the placeHolder image
         *
         * @param placeholder The drawable shows on loading image {@code uri}
         * @return class instance
         */
        public DraweeSpan.Builder setPlaceHolderImage(Element placeholder) {
            mPlaceholder = placeholder;
            return this;
        }

        /**
         * Sets placeHolder and Dimension for the image
         *
         * @return DraweeSpan
         */
        public DraweeSpan build() {
            ShapeElement placeHolder;
            if (mPlaceholder == null) {
                placeHolder = new ShapeElement();
                placeHolder.setRgbColor(RgbColor.fromArgbInt(Color.BLUE.getValue()));
                placeHolder.setBounds(0, 0, width, height);
                mPlaceholder = placeHolder;
            }
            DraweeSpan span = new DraweeSpan(uri, mPlaceholder);
            span.mLayout.modify(width, height);
            span.mMargin.set(margin.left, margin.top, margin.right, 0);
            span.layout();
            return span;
        }
    }
}
